package com.myworkspace.standard.common.lock;


import com.myworkspace.standard.common.utils.RedisClientUtil;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.support.PeriodicTrigger;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 分布式锁实现
 * 业务场景，订单支付，防止重复支付，
 * 秒杀活动 防止重复下单
 * -- reet
 * 特性：
 * 保证同一时刻，只有一台机器的一个线程在执行这段程序
 * 需要是可重入锁
 */
public class DSLock {


    /*
    加锁 lua脚本
    keys[1]:锁标识
    argv[1]:线程唯一标识
    argv[2]:过期时间，秒
    argv[3]:返回参数
        if redis.call('get',KEYS[1]) == ARGV[1] then
            redis.call('expire',KEYS[1],tonumber(ARGV[2]))
            return ARGV[3]
        elseif redis.call('SETNX',KEYS[1],ARGV[1]) ==1 then
            redis.call('expire',KEYS[1],tonumber(ARGV[2]))
            return ARGV[3]
        else
            return 0
        end
     */
    private static final String LOCK_SCRIPT =
            "if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
                    "redis.call('expire',KEYS[1],tonumber(ARGV[2]))\n" +
                    "return ARGV[3]\n" +
                    "elseif redis.call('SETNX',KEYS[1],ARGV[1]) ==1 then\n" +
                    "redis.call('expire',KEYS[1],tonumber(ARGV[2]))\n" +
                    "return ARGV[3] \n" +
                    "else\n" +
                    "return 0\n" +
                    "end";
    /*
    锁续期
        if redis.call('exists',KEYS[1]) ==1 then
            redis.call('expire',KEYS[1],tonumber(ARGV[2]))
            return ARGV[3]
        else
            return 0
        end
     */
    private static final String EXPIRE_SCRIPT =
            "if redis.call('exists',KEYS[1]) ==1 then\n" +
                    "redis.call('expire',KEYS[1],tonumber(ARGV[2]))\n" +
                    "return ARGV[3]\n" +
                    "else\n" +
                    "return 0\n" +
                    "end";


    /*
    解锁脚本
        if redis.call('get',KEYS[1]) == ARGV[1] then
            redis.call('del',KEYS[1])
            return ARGV[3]
        else
            return 0
        end
     */
    private static final String UNLOCK_SCRIPT =
            "if redis.call('get',KEYS[1]) == ARGV[1] then\n" +
                    "redis.call('del',KEYS[1])\n" +
                    "return ARGV[3]\n" +
                    "else\n" +
                    "return 0\n" +
                    "end";

    /**
     * key-value：key：线程标识，value：本地锁
     * 每一把锁都对应一个本地锁
     * 存放本地锁ReentrantLock 保证锁的可重入性
     */
    private static final Map<String, ReentrantLock> LOCAL_LOCKS = new ConcurrentHashMap<>();

    /**
     * 线程唯一标识 获取线程唯一标识
     * 通过ThreadLocal的withInitial 方法 获取ThreadLocal的子类 并传入函数式编程方法
     * 函数式编程好处，可以让你在调用的时候才写逻辑，提高了通用性
     */
    private static final ThreadLocal<String> CLIENT_ID = ThreadLocal.withInitial(
            () -> String.format("%s_%s", Thread.currentThread().getName(), UUID.randomUUID().toString().replaceAll("-", "")));


    /**
     * 点：线程池，定时任务
     * 锁的守护线程池，也是个定时任务。用来快过期时给锁续期，每次在2/3的时间执行续命任务
     * 每次加锁后注册一个定时任务，在过期时间的2/3时间处执行  todo 定时任务 稳定吗
     */
    private static class RedisLockExpireScheduler {
        private static final TaskScheduler TASK_SCHEDULER;
        private static final Map<String, ScheduledFuture> SF_MAP;

        static {
            TASK_SCHEDULER = new ConcurrentTaskScheduler(new ScheduledThreadPoolExecutor(10, new RedisLockExpireThreadFactory()));
            SF_MAP = new ConcurrentHashMap<>();
        }

        public static void register(String taskId, Runnable task, PeriodicTrigger trigger) {
            ScheduledFuture<?> sf = TASK_SCHEDULER.schedule(task, trigger);
            SF_MAP.put(taskId, sf);
        }

        public static void cancel(String taskId) {
            ScheduledFuture<?> sf = SF_MAP.get(taskId);
            if (sf != null) {
                sf.cancel(false);
            }
        }
    }


    /**
     * 处理守护线程
     */
    private static class RedisLockExpireThreadFactory implements ThreadFactory {

        private static AtomicInteger atomicInteger = new AtomicInteger(0);

        @Override
        public Thread newThread(Runnable r) {
            int c = atomicInteger.incrementAndGet();
            System.out.println("create no " + c + " Threads");
            return new WorkThread(r, atomicInteger);//通过计数器，可以更好的管理线程
        }
    }

    /**
     * 这也算是一种代理模式，Thread代理 Runnable执行
     */
    private static class WorkThread extends Thread {
        private final Runnable target;   //线程执行目标
        private final AtomicInteger counter;

        public WorkThread(Runnable target, AtomicInteger counter) {
            this.target = target;
            this.counter = counter;
        }

        @Override
        public void run() {
            try {
                target.run();
            } finally {
                int c = counter.getAndDecrement();
                System.out.println("terminate no " + c + " Threads");
            }
        }
    }


    /**
     * 加锁
     * 获取本地锁
     *
     * @param key 锁key
     */
    public static void lock(String key) {
        LOCAL_LOCKS.putIfAbsent(key, new ReentrantLock());
        ReentrantLock localLock = LOCAL_LOCKS.get(key);
        localLock.lock();
        while (true) {
            // 使用lua脚本(保证原子性)获取 redis锁
            List<String> keys = new LinkedList<>();
            keys.add(key);
            String clientId = CLIENT_ID.get();
            String execute = RedisClientUtil.execute(LOCK_SCRIPT, keys, clientId, 30, "1");
            if ("1".equals(execute)) {
                if (localLock.getHoldCount() == 1) {
                    PeriodicTrigger trigger = new PeriodicTrigger(20, TimeUnit.SECONDS);
                    trigger.setInitialDelay(20);
                    // 开启续命线程
                    RedisLockExpireScheduler.register(clientId, () -> {
                                RedisClientUtil.execute(EXPIRE_SCRIPT, keys, clientId, 30, "1");
                            }, trigger
                    );
                }
                break;
            } else {
                // 其他线程占用锁 等待若干毫秒后重新获取锁
                try {
                    TimeUnit.MICROSECONDS.sleep(20);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Around("@annotation(com.com.myworkspace.standard.common.lock.RedisLock)")
    public Object lockPointAround(ProceedingJoinPoint point) throws Throwable {
        System.out.println("执行前");
        Object proceed = point.proceed();
        System.out.println("执行后");
        return proceed;
    }

    public static void unLock(String key) {
        String clientId = CLIENT_ID.get();
        ReentrantLock localLock = LOCAL_LOCKS.get(key);
        if (!localLock.isHeldByCurrentThread()) {
            throw new IllegalStateException(String.format("clientId[%s]未持有锁", clientId));
        }
        if (localLock.getHoldCount() > 1) {
            // 若本地锁大于1则只释放本地锁
            localLock.unlock();
        } else {
            try {
                List<String> keys = new LinkedList<>();
                keys.add(key);
                // 本地锁等于1 释放redis锁
                String execute = RedisClientUtil.execute(UNLOCK_SCRIPT, keys, clientId, 30, "1");
                if ("1".equals(execute)) {
                    RedisLockExpireScheduler.cancel(clientId);
                }
            } finally {
                localLock.unlock();
            }
        }
    }


    class Test extends Thread {
        @Override
        public void run() {
            super.run();
        }
    }
}
